﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.IO;
using CSNN.GUI;

namespace CSNN
{
    /// <summary>
    /// Класс нейронной сети
    /// </summary>
    public class NeuralNetwork
    {
        #region Члены класса
        /// <summary>
        /// Построенна ли сеть
        /// </summary>
        public bool IsBuild { get; private set; }
                
        /// <summary>
        /// Количество нейронных слоев в сети
        /// </summary>
        public int LayersCount { get; private set; }

        /// <summary>
        /// Нейронные слои данной сети
        /// </summary>
        public NeuralLayer[] NeuralLayers;

        /// <summary>
        /// Количество нейронов в слоях
        /// </summary>
        private int[] NeuronsInLayers;

        /// <summary>
        /// Размерность нейроннов в слоях
        /// </summary>
        private int[] LayersDemensions;

        /// <summary>
        /// Количество нейроннов в сети
        /// </summary>
        public int NeuronsInNet { get; private set; }

        /// <summary>
        /// Список обучающих образов данной сети
        /// </summary>
        public BindingList<TrainingImageInList> TrainingImages;

        /// <summary>
        /// Область случайных значений для инициализации весов
        /// </summary>
        private int _randMin, _randMax;

        /// <summary>
        /// Флаг использования случайных значений
        /// </summary>
        private bool _isRandom, isDoubleRandom;

        /// <summary>
        /// Флаг обученности сети
        /// </summary>
        public bool IsEducate;

        /// <summary>
        /// Значение количества циклов при обучении
        /// </summary>
        public uint EducationCyclesCount;

        #endregion

        #region Конструкторы

        public NeuralNetwork()
        {
            this.LayersCount = 0;
            this.NeuralLayers = null;
            this.NeuronsInLayers = null;
            this.LayersDemensions = null;
            this.TrainingImages = new BindingList<TrainingImageInList>();
            this._isRandom = false;
            this.IsEducate = false;
            this.EducationCyclesCount = 0;
        }

        #endregion

        #region Основные методы

        /// <summary>
        /// Установка отрезка случайных величин для инициализации весов
        /// </summary>
        /// <param name="randMin">Минимальное случайное значение</param>
        /// <param name="randMax">Максимальное случайное значение</param>        
        public void SetRandomSegment(int randMin, int randMax)
        {
            if (randMin >= randMax)
                throw new ArgumentException("Ошибка!\nНекорректное определение диапозона случайных значений!");
            this._randMin = randMin;
            this._randMax = randMax;
            this._isRandom = true;
        }

        public void SetDoubleUnitRandomSegment()
        {
            this._isRandom = true;
            this.isDoubleRandom = true;
        }

        /// <summary>
        /// Установка слоев нейронной сети
        /// </summary>
        /// <param name="NeuronsInLayers">Количество нейроннов в сети</param>
        /// <param name="LayersDemensions">Размерность нейронов в слоях</param>
        public void SetNeuralNetwork(int LayersCount, int[] NeuronsInLayers, int InputImageDemension)
        {
            if (LayersCount <= 0 || NeuronsInLayers == null || InputImageDemension <= 0)
            {
                throw new ArgumentException("Ошибка!\nЗначения аргуметов определния нейронной сети имеют недопустимые значения.");
            }

            this.IsBuild = true;

            this.LayersCount = LayersCount;
            this.NeuralLayers = new NeuralLayer[LayersCount];
            this.LayersDemensions = new int[LayersCount];

            this.LayersDemensions[0] = InputImageDemension;
            for (int i = 0; i < LayersCount; i++)
            {
                if (i != 0) this.LayersDemensions[i] = NeuronsInLayers[i - 1];
                this.NeuralLayers[i] = new NeuralLayer(NeuronsInLayers[i], this.LayersDemensions[i]);
            }

            this.NeuronsInLayers = new int[LayersCount];
            NeuronsInLayers.CopyTo(this.NeuronsInLayers, 0);

            for (int i = 0; i < this.LayersCount; i++)
                this.NeuronsInNet += this.NeuralLayers[i].NeuronsCount;

            Random rnd = new Random();
            if (_isRandom)
            {
                for (int i = 0; i < LayersCount; i++)
                {
                    for (int j = 0; j < NeuralLayers[i].NeuronsCount; j++)
                    {
                        for (int c = 0; c < NeuralLayers[i].NeuronsDemension; c++)
                        {
                            if (isDoubleRandom)
                            {
                                bool isSubZero = false;
                                if (rnd.Next(-1,1) == 0) isSubZero = true;
                                double NewValue = rnd.NextDouble();
                                if (isSubZero)
                                {
                                    NewValue = -NewValue;
                                    isSubZero = false;
                                }
                                WeightCorrection(i, j, c, NewValue);
                            }
                            else
                            {
                                WeightCorrection(i, j, c, (double)rnd.Next(_randMin, _randMax));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Функция подачи образа на нейронную сеть
        /// </summary>
        /// <param name="InputImage">Входной образ нейронной сети</param>
        /// <returns>Выходной образ нейронной сети</returns>
        public Image GetNeuralNetworkOut(Image InputImage)
        {
            if (InputImage == null)
            {
                throw new ArgumentNullException("Ошибка! Образ не определен.");
            }
            if (InputImage.InSignalsCount != this.LayersDemensions[0])
            {
                throw new ArgumentException("Ошибка! Размерность входного образа не соответсвует размерности входного вектора данной нейронной сети.");
            }

            Image OutImage = InputImage;

            for (int i = 0; i < this.LayersCount; i++)
            {
                OutImage = NeuralLayers[i].GetOutImage(OutImage);
            }

            return OutImage;
        }

        /// <summary>
        /// Корректировка весового коэффициэнта нейронной сети
        /// </summary>
        /// <param name="LayersNum">Номер слоя нейронной сети</param>
        /// <param name="NeuronNum">Номер нейронна в слое</param>
        /// <param name="WeightNum">Номер весового коэффициэнта нейрона</param>
        /// <param name="NewWeightValue">Новое значение весового коэффициэнта</param>
        public void WeightCorrection(int LayersNum, int NeuronNum, int WeightNum, double NewWeightValue)
        {
            this.NeuralLayers[LayersNum].WeightCorrection(NeuronNum, WeightNum, NewWeightValue);
        }

        #endregion

        #region Обучение

        /// <summary>
        /// Обучение обратным распространением ошибки одним обучающим образом
        /// </summary>
        /// <param name="InputImage">Обучающий образ</param>
        private void BackPropagationStep(TrainingImage InputImage, ref double SumQudraError, ref int ErrorsCount, int MaxCycles)
        {
            SumQudraError = 0;
            ErrorsCount = 0;

            double EduSpeed = 1;    // Скорость обучения

            Image T = GetNeuralNetworkOut(InputImage);

            double[][] neuronsOuts = new double[this.LayersCount][];
            for (int i = 0; i < this.LayersCount; i++)
                neuronsOuts[i] = new double[this.NeuralLayers[i].NeuronsCount];

            Image image = new Image();

            // Получение выходов каждого нейрона
            for (int i = 0; i < this.LayersCount; i++)
            {
                image.InputSignals = new double[this.NeuralLayers[i].NeuronsDemension];

                if (i == 0)
                {
                    InputImage.InputSignals.CopyTo(image.InputSignals, 0);
                }
                else
                {
                    for (int j = 0; j < this.NeuralLayers[i - 1].NeuronsCount; j++)
                    {
                        image.InputSignals[j] = neuronsOuts[i - 1][j];
                    }
                }

                for (int j = 0; j < this.NeuralLayers[i].NeuronsCount; j++)
                {
                    neuronsOuts[i][j] = this.NeuralLayers[i].Neurons[j].GetOutSignal(image);
                }
            }

            double[][] delta = new double[this.LayersCount][];  // Массив массивов ошибок для каждого нейрона
            for (int i = 0; i < this.LayersCount; i++)
                delta[i] = new double[this.NeuralLayers[i].NeuronsCount];

            // Расчет ошибки для каждого нейрона
            for (int i = this.LayersCount - 1; i >= 0; i--)
            {
                for (int j = 0; j < this.NeuralLayers[i].NeuronsCount; j++)
                {
                    if (i == this.LayersCount - 1)
                    {
                        delta[i][j] = T.InputSignals[j] * (1 - T.InputSignals[j]) * (InputImage.OutputSignals[j] - T.InputSignals[j]);
                        double error = InputImage.OutputSignals[j] - T.InputSignals[j];
                        if (error != 0)
                        {
                            SumQudraError += delta[i][j] * delta[i][j];
                            ErrorsCount++;
                        }
                    }
                    else
                    {
                        delta[i][j] = 0;
                        double ErrorSum = 0;
                        for (int c = 0; c < this.NeuralLayers[i + 1].NeuronsCount; c++)
                        {
                            ErrorSum += delta[i + 1][c] * this.NeuralLayers[i + 1].Neurons[c].Synapses[j];
                        }
                        delta[i][j] += neuronsOuts[i][j] * (1 - neuronsOuts[i][j]) * ErrorSum;
                    }
                }
            }

            // Корректировка весов
            for (int i = 0; i < this.LayersCount; i++)
            {
                for (int j = 0; j < this.NeuralLayers[i].NeuronsCount; j++)
                {
                    for (int c = 0; c < this.NeuralLayers[i].Neurons[j].SynapseCount; c++)
                    {
                        if (i == 0)
                        {
                            this.NeuralLayers[i].Neurons[j].Synapses[c] += EduSpeed * delta[i][j] * InputImage.InputSignals[c];
                        }
                        else if (i == this.LayersCount - 1)
                        {
                            this.NeuralLayers[i].Neurons[j].Synapses[c] += EduSpeed * delta[i][j] * InputImage.OutputSignals[j];
                        }
                        else
                        {
                            this.NeuralLayers[i].Neurons[j].Synapses[c] += EduSpeed * delta[i][j] * neuronsOuts[i - 1][c];
                        }
                    }
                    this.NeuralLayers[i].Neurons[j].Synapses[this.NeuralLayers[i].Neurons[j].SynapseCount] += EduSpeed * delta[i][j] * 1;
                }
            }
        }

        /// <summary>
        /// Обучение сети методом обратного распространения обуающим множеством, созданным для данной сети
        /// </summary>
        /// <param name="MaxCycles">Максимальное количество циклов обучения</param>
        /// <param name="AllowableError">Допустимое значение ошибки</param>        
        public uint EducationByBackPropogation(int MaxCycles, double AllowableError)
        {
            uint i = 0;

            while (true)
            {
                int MegaErrorsCount = 0;
                double MegaSumQuadraError = 0;

                foreach (TrainingImageInList tImage in this.TrainingImages)
                {
                    double SumQudraError = 0;
                    int ErrorsCount = 0;
                    BackPropagationStep(tImage.Image, ref SumQudraError, ref ErrorsCount, MaxCycles);

                    MegaErrorsCount += ErrorsCount;
                    MegaSumQuadraError += SumQudraError;
                }
                                
                i++;

                if (i > MaxCycles)
                {
                    return 0;
                }

                double UltraError = Math.Sqrt(MegaSumQuadraError / MegaErrorsCount);

                if (UltraError < AllowableError)
                {
                    return i;
                }
            }
        }

        #endregion
        
        #region Сохранение/Загрузка нейронной сети в/из файла

        /// <summary>
        /// Запись нейронной сети в файл
        /// </summary>
        /// <param name="file">Имя файла</param>
        public void SaveToFile(string fileName)
        {
            using (StreamWriter writeStream = new StreamWriter(fileName))
            {
                // Запись количества слоев и размерности входного образа нейронной сети
                writeStream.WriteLine("{0:d};{1:d}", this.LayersCount, this.LayersDemensions[0]);
                
                // Запись флага заполнения случайными значениями и самих значений
                writeStream.WriteLine(this._isRandom);
                if (_isRandom)
                {
                    writeStream.WriteLine("{0:d};{1:d}", this._randMin, this._randMax);
                }
                
                // Запись размерности каждого слоя нейронной сети со значениями синапсов
                for (int iLayerNum = 0; iLayerNum < this.LayersCount; iLayerNum++)
                {
                    writeStream.WriteLine("{0:d}", this.NeuralLayers[iLayerNum].NeuronsCount);
                    for (int iNeuronNum = 0; iNeuronNum < this.NeuralLayers[iLayerNum].NeuronsCount; iNeuronNum++)
                    {
                        for (int iSynapseNum = 0; iSynapseNum < this.NeuralLayers[iLayerNum].Neurons[iNeuronNum].SynapseCount; iSynapseNum++)
                        {
                            writeStream.Write(this.NeuralLayers[iLayerNum].Neurons[iNeuronNum].Synapses[iSynapseNum].ToString() + ";");
                        }
                        writeStream.WriteLine("");
                    }
                    
                }

            }
        }

        /// <summary>
        /// Загрузка нейронной сети из файла
        /// </summary>
        /// <param name="file">Имя файла</param>
        public void LoadFromFile(string fileName)
        {
            using (StreamReader readerStream = new StreamReader(fileName))
            {
                string sError = "Ошибка! Не корректные данные в загружаемом файле!";
                
                string[] values;
                string inputLine;

                if ((inputLine = readerStream.ReadLine()) != null)
                {
                    values = inputLine.Split(';');
                }
                else
                {
                    throw new FileLoadException("Ошибка! Файл пуст.");
                }

                int nnLayers = 0, nnInputs = 0;
                bool isRand = false;
                int randMin = 0, randMax = 0;

                if ((Int32.TryParse(values[0], out nnLayers) != true) || (Int32.TryParse(values[1], out nnInputs) != true))
                {
                    throw new InvalidDataException(sError);
                }
                else
                {
                    inputLine = readerStream.ReadLine();
                    if (!Boolean.TryParse(inputLine, out isRand))
                    {
                        throw new InvalidDataException(sError);
                    }
                    if (isRand)
                    {
                        inputLine = readerStream.ReadLine();
                        values = inputLine.Split(';');
                        if ((Int32.TryParse(values[0], out randMin) != true) || (Int32.TryParse(values[1], out randMax) != true))
                        {
                            throw new InvalidDataException(sError);
                        }
                    }
                    int[] intLayersNeuronsCount = new int[nnLayers];
                    double[][][] synapses = new double[nnLayers][][];

                    for (int iLayerNum = 0; iLayerNum < nnLayers; iLayerNum++)
                    {
                        if (Int32.TryParse(readerStream.ReadLine(), out intLayersNeuronsCount[iLayerNum]) != true)
                        {
                            throw new InvalidDataException(sError);
                        }
                        else
                        {
                            synapses[iLayerNum] = new double[intLayersNeuronsCount[iLayerNum]][];
                            for (int iNeuronNum = 0; iNeuronNum < intLayersNeuronsCount[iLayerNum]; iNeuronNum++)
                            {
                                inputLine = readerStream.ReadLine();
                                values = inputLine.Split(';');

                                if (iLayerNum == 0)
                                {   
                                    synapses[iLayerNum][iNeuronNum] = new double[nnInputs];

                                    if (values.Length - 1 != nnInputs)
                                        throw new InvalidDataException(sError);

                                    for (int k = 0; k < nnInputs; k++)
                                    {
                                        if (!Double.TryParse(values[k], out synapses[iLayerNum][iNeuronNum][k]))
                                            throw new InvalidDataException(sError);
                                    }
                                }
                                else
                                {
                                    synapses[iLayerNum][iNeuronNum] = new double[intLayersNeuronsCount[iLayerNum-1]];

                                    if (values.Length - 1 != intLayersNeuronsCount[iLayerNum-1])
                                        throw new InvalidDataException(sError);

                                    for (int k = 0; k < intLayersNeuronsCount[iLayerNum-1]; k++)
                                    {
                                        if (!Double.TryParse(values[k], out synapses[iLayerNum][iNeuronNum][k]))
                                            throw new InvalidDataException(sError);
                                    }
                                }
                            }
                        }
                    }

                    if (_isRandom)
                    {
                        this.SetRandomSegment(_randMin, _randMax);
                    }
                    this.SetNeuralNetwork(nnLayers, intLayersNeuronsCount, nnInputs);
                    for (int i = 0; i < nnLayers; i++)
                    {
                        for (int j = 0; j < intLayersNeuronsCount[i]; j++)
                        {
                            synapses[i][j].CopyTo(this.NeuralLayers[i].Neurons[j].Synapses, 0);
                        }
                    }

                    
                }
            }
        }

        #endregion


    }
}
